[Разбор] Разбор генератора паролей с проверкой надёжности произвольного пароля

Введение

В эпоху цифровых технологий надёжность пароля стала одним из ключевых факторов безопасности личных данных и онлайн-аккаунтов. Однако создавать действительно устойчивые к взлому пароли непросто: многие пользователи выбирают простые и легко угадываемые комбинации, что существенно снижает уровень защиты.

В этой статье мы подробно разберём современный генератор паролей с гибкими настройками, который позволяет создавать надёжные и уникальные пароли с учётом пожеланий пользователя. Особенностью сервиса является интеграция библиотеки zxcvbn – инструмента для оценки и проверки стойкости любого произвольного пароля. Она позволяет получить объективную оценку безопасности и рекомендует способы усиления пароля.

Используемые технологии и библиотеки

1. HTML5 и CSS3

HTML5 – это язык разметки веб-страниц, позволяющий структурировать контент и задавать семантику. В коде используется семантическая разметка, теги и атрибуты, обеспечивающие правильную организацию формы генератора паролей.

CSS3 – это язык стилей, который отвечает за внешний вид страницы. Здесь применены современные возможности, такие как CSS-переменные, адаптивная верстка (медиа-запросы), псевдоэлементы и переходы, что позволяет реализовать светлую и тёмную тему и удобный интерфейс.

2. JavaScript (ES6+)

Язык программирования, встроенный в браузеры, используется для динамического управления страницей. В коде применяется современный синтаксис ES6+ (например, constlet, стрелочные функции, работа с массивами и объектами) для реализации логики генерации пароля, обработки событий интерфейса, валидации и сохранения настроек.


3. Web Crypto API (crypto.getRandomValues())

Это встроенный в браузеры API для безопасной и криптографически стойкой генерации случайных чисел. Он играет критическую роль в генерации паролей, обеспечивая высокий уровень случайности и безопасности, значительно превосходящий обычный Math.random().


4. LocalStorage

API браузера для хранения данных локально на стороне клиента. Используется для сохранения пользовательских настроек (выбранной длины пароля, включённых групп символов, темы и прочего), чтобы при повторном посещении сайта эти настройки автоматом применялись, улучшая пользовательский опыт.


5. Библиотека zxcvbn (версия 4.4.2)

Это открытая JavaScript-библиотека от Dropbox для оценки надёжности паролей. zxcvbn анализирует пароль по множеству параметров, включая частотность слов, шаблоны повторений, последовательности и словарные слова, и выдаёт оценку устойчивости (от 0 до 4), энтропию, предупреждения и рекомендации. В проекте используется для объективной проверки паролей.

Возможности программы

Обзор Кода

Генерация паролей с использованием криптографически стойких случайных чисел

Оценка надёжности пароля

Оценка надёжности пароля в этом коде происходит с помощью библиотеки zxcvbn.js – мощного инструмента для анализа сложности пароля с учётом вероятных шаблонов и слабых мест.

Вот подробный разбор, как именно работает оценка:

  1. Ввод пароля для проверки и начальная валидация
  2. Если поле пустое – выводится просьба ввести пароль.
  3. Если длина пароля превышает 128 символов – выводится ошибка о максимальной длине.
  4. Запуск оценки пароля с помощью zxcvbn
  5. entropy – оценка энтропии (битовой стойкости) пароля: чем выше, тем сложнее подобрать.
  6. score – числовая оценка от 0 (очень слабый) до 4 (отличный), характеризующая надёжность.
  7. feedback – объект с предупреждениями и рекомендациями для пользователя.
  8. Формирование вывода оценки
  9. Если score от 3 и выше – показывается текст "Высокая надёжность".
  10. Если entropy равен 0, но score > 0 – показывается "10+ бит".
  11. Иначе показывается конкретное значение энтропии в битах, напр. 45 бит.
  12. Предупреждения и рекомендации
  13. «Это очень распространённый пароль»
  14. «Прямые ряды клавиш, например qwerty»
  15. «Добавьте ещё одно-два слова, лучше редко встречающиеся.»
  16. «Избегайте повторяющихся слов и символов.»
  17. Защита от XSS
  18. Интерактивность и отзывы
  19. Библиотека zxcvbn оценивает даже сложные шаблоны, например последовательности, повторения, предсказуемые слова, даты, и даёт объективную меру сложности – энтропию в битах.
  20. Оценка score от 0 до 4 – простой красочный индикатор уровня.
  21. Предупреждения и рекомендации помогают пользователю сделать пароль сильнее.

Цифры от 0 до 4 в оценке надёжности пароля (поле score из zxcvbn) означают уровни стойкости пароля:

Полный код генератора

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>Генератор паролей с спецсимволами по условию</title>
<style>
  :root {
    --color-bg-light: #f0f4f8;
    --color-text-light: #222;
    --color-card-light: #fff;
    --color-primary-light: #4a90e2;
    --color-primary-hover-light: #357abd;
    --color-toggle-bg-light: #ccc;

    --color-bg-dark: #121212;
    --color-text-dark: #eee;
    --color-card-dark: #1e1e1e;
    --color-primary-dark: #4a90e2;
    --color-primary-hover-dark: #357abd;
    --color-toggle-bg-dark: #555;
  }
  body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    margin: 0;
    padding: 1rem;
    background-color: var(--color-bg-light);
    color: var(--color-text-light);
    transition: background-color 0.3s ease, color 0.3s ease;
    display: flex;
    justify-content: center;
  }
  .container {
    background-color: var(--color-card-light);
    padding: 2rem 1.5rem;
    border-radius: 12px;
    max-width: 460px;
    width: 100%;
    box-sizing: border-box;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
    transition: background-color 0.3s ease;
  }
  h1 {
    text-align: center;
    margin-bottom: 1.5rem;
    font-weight: 700;
    font-size: 1.8rem;
  }
  label {
    display: block;
    margin-bottom: 0.4rem;
    font-weight: 600;
  }
  select, input[type="text"] {
    width: 100%;
    padding: 0.55rem 0.75rem;
    border-radius: 8px;
    border: 1.8px solid #bbb;
    font-size: 1rem;
    font-weight: 500;
    box-sizing: border-box;
    transition: border-color 0.3s ease;
    color: var(--color-text-light);
    background-color: var(--color-card-light);
  }
  select:focus, input[type="text"]:focus {
    outline: none;
    border-color: var(--color-primary-light);
  }
  .toggle-group {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 1rem 0 0.7rem;
  }
  .toggle-label {
    font-size: 1rem;
    font-weight: 500;
  }
  .switch {
    position: relative;
    display: inline-block;
    width: 48px;
    height: 26px;
  }
  .switch input {
    opacity: 0;
    width: 0;
    height: 0;
  }
  .slider {
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    background-color: var(--color-toggle-bg-light);
    border-radius: 26px;
    cursor: pointer;
    transition: background-color 0.3s ease;
  }
  .slider:before {
    content: "";
    position: absolute;
    height: 20px;
    width: 20px;
    left: 3px;
    bottom: 3px;
    background-color: white;
    border-radius: 50%;
    transition: transform 0.3s ease;
  }
  input:checked + .slider {
    background-color: var(--color-primary-light);
  }
  input:checked + .slider:before {
    transform: translateX(22px);
  }
  button {
    margin-top: 1.8rem;
    width: 100%;
    padding: 14px 0;
    font-size: 1.15rem;
    font-weight: 700;
    color: white;
    border: none;
    background-color: var(--color-primary-light);
    border-radius: 12px;
    cursor: pointer;
    box-shadow: 0 4px 14px rgba(74,144,226,0.65);
    transition: background-color 0.3s ease;
  }
  button:hover:not(:disabled) {
    background-color: var(--color-primary-hover-light);
  }
  button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
  #result {
    margin-top: 1.6rem;
    padding: 15px 12px;
    background: #e1ecf9;
    border-radius: 12px;
    word-break: break-word;
    font-weight: 700;
    font-size: 1.3rem;
    user-select: all;
    color: #1c2a4a;
    transition: background-color 0.3s ease, color 0.3s ease;
    min-height: 1.6em;
  }
  #entropy {
    margin-top: 0.8rem;
    font-size: 1rem;
    font-weight: 600;
    color: #333;
    text-align: center;
  }
  #checkPassword, #checkBtn {
    margin-top: 1rem;
  }
  #checkPassword {
    padding: 0.55rem 0.75rem;
    font-size: 1rem;
    border-radius: 8px;
    border: 1.8px solid #bbb;
    box-sizing: border-box;
    width: 100%;
    color: var(--color-text-light);
    background-color: var(--color-card-light);
    transition: border-color 0.3s ease;
  }
  #checkPassword:focus {
    outline: none;
    border-color: var(--color-primary-light);
  }
  #checkResult {
    margin-top: 0.6rem;
    font-weight: 600;
    font-size: 1rem;
    text-align: center;
    opacity: 1;
    transition: opacity 0.3s ease;
    min-height: 1.2em;
  }
  #checkSuggestions {
    margin-top: 0.3rem;
    font-size: 0.9rem;
    color: #555;
    line-height: 1.3;
    padding: 0 10px;
    text-align: left;
  }
  #copyBtn {
    margin-top: 0.8rem;
    background-color: #6a9be3;
    border-radius: 10px;
    padding: 10px;
    font-weight: 600;
    font-size: 1rem;
    border: none;
    cursor: pointer;
    color: white;
    box-shadow: 0 2px 10px rgba(74,144,226,0.7);
    transition: background-color 0.3s ease;
    width: 100%;
  }
  #copyBtn:hover {
    background-color: #547fca;
  }
  #symbolsHint {
    font-size: 0.85rem;
    color: #666;
    margin-top: 0.2rem;
    user-select: none;
  }
  body.dark {
    background-color: var(--color-bg-dark);
    color: var(--color-text-dark);
  }
  body.dark .container {
    background-color: var(--color-card-dark);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.8);
  }
  body.dark select, body.dark input[type="text"] {
    border-color: #555;
    background-color: var(--color-card-dark);
    color: var(--color-text-dark);
  }
  body.dark select:focus, body.dark input[type="text"]:focus {
    border-color: var(--color-primary-dark);
  }
  body.dark .slider {
    background-color: var(--color-toggle-bg-dark);
  }
  body.dark input:checked + .slider {
    background-color: var(--color-primary-dark);
  }
  body.dark #result {
    background: #2b3a67;
    color: #bcdfff;
  }
  body.dark #entropy {
    color: #b0c6f6;
  }
  body.dark button {
    background-color: var(--color-primary-dark);
    box-shadow: 0 4px 14px rgba(74,144,226,0.85);
  }
  body.dark button:hover:not(:disabled) {
    background-color: #2e5ea6;
  }
  body.dark #copyBtn {
    background-color: #547fca;
    box-shadow: 0 2px 10px rgba(74,144,226,0.85);
  }
  body.dark #copyBtn:hover {
    background-color: #4367a4;
  }
  .theme-switch-wrapper {
    margin-bottom: 1.3rem;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    font-weight: 600;
    font-size: 1rem;
    user-select: none;
  }
  .theme-switch-wrapper .switch {
    width: 52px;
    height: 28px;
  }
  .theme-label {
    cursor: pointer;
  }
  @media (max-width: 480px) {
    .container {
      padding: 1.6rem 1.2rem;
      border-radius: 10px;
      max-width: 100%;
    }
    h1 {
      font-size: 1.5rem;
    }
    button {
      font-size: 1.05rem;
    }
    #result {
      font-size: 1.15rem;
    }
    #copyBtn {
      font-size: 1rem;
      padding: 9px;
    }
  }
  .hidden {
    opacity: 0;
  }
</style>
</head>
<body>
<div class="container" role="main" aria-label="Генератор паролей">

  <div class="theme-switch-wrapper">
    <label class="theme-label" for="theme-switch">Темная тема</label>
    <label class="switch">
      <input type="checkbox" id="theme-switch" aria-checked="false" role="switch" />
      <span class="slider"></span>
    </label>
  </div>

  <h1>Генератор паролей</h1>

  <label for="length">Количество символов:</label>
  <select id="length" name="length" aria-describedby="length-desc"></select>
  <div id="length-desc" style="font-size:0.85rem; color:#666; margin-bottom:0.7rem;">Минимум 12, максимум 64 символа</div>

  <div class="toggle-group">
    <span class="toggle-label">0-9</span>
    <label class="switch" title="Включить цифры" aria-label="Включить цифры">
      <input type="checkbox" id="numbers" />
      <span class="slider"></span>
    </label>
  </div>
  <div class="toggle-group">
    <span class="toggle-label">A-Z</span>
    <label class="switch" title="Включить заглавные буквы" aria-label="Включить заглавные буквы">
      <input type="checkbox" id="uppercase" />
      <span class="slider"></span>
    </label>
  </div>
  <div class="toggle-group">
    <span class="toggle-label">a-z</span>
    <label class="switch" title="Включить строчные буквы" aria-label="Включить строчные буквы">
      <input type="checkbox" id="lowercase" />
      <span class="slider"></span>
    </label>
  </div>
  <div class="toggle-group" id="hex-toggle-wrapper">
    <span class="toggle-label">Шестнадцатеричные (0-9, A-F)</span>
    <label class="switch" title="Включить шестнадцатеричные символы" aria-label="Включить шестнадцатеричные символы">
      <input type="checkbox" id="hexadecimal" />
      <span class="slider"></span>
    </label>
  </div>
  <div class="toggle-group">
    <span class="toggle-label">Исключить похожие символы (i l 1 L o 0 O)</span>
    <label class="switch" title="Исключить похожие символы" aria-label="Исключить похожие символы">
      <input type="checkbox" id="excludeSimilar" />
      <span class="slider"></span>
    </label>
  </div>

  <div class="toggle-group">
    <span class="toggle-label">Включить специальные символы</span>
    <label class="switch" title="Включить специальные символы" aria-label="Включить специальные символы">
      <input type="checkbox" id="enableSymbols" />
      <span class="slider"></span>
    </label>
  </div>
  <div style="margin-top:-0.7rem; margin-bottom:1rem; font-size:0.85rem; color:#666; user-select:none;">
    Используются символы: !@#$%^&amp;*
  </div>

  <div>
    <label for="symbols">Дополнительные спецсимволы (макс. 30 символов):</label>
    <input type="text" id="symbols" placeholder="Можно ввести свои символы" autocomplete="off" spellcheck="false" maxlength="30" aria-describedby="symbolsHint" />
    <div id="symbolsHint" style="font-size:0.85rem; color:#666; margin-top:0.2rem; user-select:none;">
      Можно вводить любые печатные ASCII символы, например: !@#$%^&amp;*()-_=+[]{}|;:,.&lt;&gt;?/\\
    </div>
  </div>

  <button id="generateBtn" disabled aria-disabled="true">Сгенерировать</button>

  <p id="result" title="Результат генерации пароля" aria-live="polite" aria-atomic="true"></p>
  <button id="copyBtn" disabled aria-disabled="true">Скопировать пароль</button>
  <p id="entropy" aria-live="polite" aria-atomic="true"></p>

  <hr style="margin: 2rem 0" />

  <label for="checkPassword">Проверить пароль на надёжность:</label>
  <input 
    type="text" id="checkPassword" placeholder="Введи или вставь пароль" autocomplete="off" spellcheck="false"
    maxlength="128" aria-describedby="checkPasswordDesc" />
  <div id="checkPasswordDesc" style="font-size:0.85rem; color:#666; margin-bottom:0.3rem;">
    Максимальная длина пароля для проверки: 128 символов
  </div>
  <button id="checkBtn">Проверить</button>
  <p id="checkResult" aria-live="polite" aria-atomic="true"></p>
  <div id="checkSuggestions"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/zxcvbn.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
  const lengthSelect = document.getElementById('length');
  const numbersCheckbox = document.getElementById('numbers');
  const uppercaseCheckbox = document.getElementById('uppercase');
  const lowercaseCheckbox = document.getElementById('lowercase');
  const hexadecimalCheckbox = document.getElementById('hexadecimal');
  const excludeSimilarCheckbox = document.getElementById('excludeSimilar');
  const enableSymbolsCheckbox = document.getElementById('enableSymbols');
  const symbolsInput = document.getElementById('symbols');
  const resultEl = document.getElementById('result');
  const entropyEl = document.getElementById('entropy');
  const copyBtn = document.getElementById('copyBtn');
  const generateBtn = document.getElementById('generateBtn');
  const themeSwitch = document.getElementById('theme-switch');
  const hexToggleWrapper = document.getElementById('hex-toggle-wrapper');

  const checkPasswordInput = document.getElementById('checkPassword');
  const checkBtn = document.getElementById('checkBtn');
  const checkResult = document.getElementById('checkResult');
  const checkSuggestions = document.getElementById('checkSuggestions');

  // Заполнить длину пароля с 12 по 64
  if(lengthSelect.options.length === 0) {
    for(let i=12; i<=64; i++) {
      lengthSelect.insertAdjacentHTML('beforeend', `<option value="${i}">${i}</option>`);
    }
    lengthSelect.value = 12;
  }

  const STORAGE_KEY = 'passwordGeneratorSettings';

  const translations = {
    warning: {
      "This is a top-10 common password": "Это один из 10 наиболее распространённых паролей",
      "This is a very common password": "Это очень распространённый пароль",
      "This is similar to a commonly used password": "Это похоже на часто используемый пароль",
      "Straight rows of keys like qwerty": "Прямые ряды клавиш, например qwerty",
      "Short keyboard patterns are easy to guess": "Короткие клавиатурные паттерны легко угадать",
      "Repeats like abcabcabc": "Повторы символов, например abcabcabc",
      "Sequence of characters": "Последовательность символов",
      "Contains recent year": "Содержит актуальный год",
      "Contains dates or years": "Содержит даты или годы"
    },
    suggestions: {
      "Add another word or two. Uncommon words are better.": "Добавьте ещё одно-два слова, лучше редко встречающиеся.",
      "Avoid repeated words and characters.": "Избегайте повторяющихся слов и символов.",
      "Avoid sequences like abc or 123.": "Избегайте последовательностей вроде abc или 123.",
      "Avoid recent years.": "Избегайте актуальных годов.",
      "Use a longer keyboard pattern with more variation.": "Используйте более длинный паттерн с большей вариативностью."
    }
  };

  function translateWarning(warning) {
    return translations.warning[warning] || warning;
  }
  function translateSuggestion(suggestion) {
    return translations.suggestions[suggestion] || suggestion;
  }

  function applyTheme(dark) {
    if(dark) {
      document.body.classList.add('dark');
      themeSwitch.checked = true;
      themeSwitch.setAttribute('aria-checked', 'true');
    } else {
      document.body.classList.remove('dark');
      themeSwitch.checked = false;
      themeSwitch.setAttribute('aria-checked', 'false');
    }
  }

  function loadSettings() {
    try {
      const saved = localStorage.getItem(STORAGE_KEY);
      if(!saved) return;
      const settings = JSON.parse(saved);
      if(typeof settings.darkTheme === 'boolean') {
        applyTheme(settings.darkTheme);
      }
      if(typeof settings.length === 'number' && settings.length >= 12) lengthSelect.value = settings.length;
      if(typeof settings.numbers === 'boolean') numbersCheckbox.checked = settings.numbers;
      if(typeof settings.uppercase === 'boolean') uppercaseCheckbox.checked = settings.uppercase;
      if(typeof settings.lowercase === 'boolean') lowercaseCheckbox.checked = settings.lowercase;
      if(typeof settings.hexadecimal === 'boolean') hexadecimalCheckbox.checked = settings.hexadecimal;
      if(typeof settings.excludeSimilar === 'boolean') excludeSimilarCheckbox.checked = settings.excludeSimilar;
      if(typeof settings.enableSymbols === 'boolean') enableSymbolsCheckbox.checked = settings.enableSymbols;
      if(typeof settings.symbols === 'string') symbolsInput.value = settings.symbols;
    } catch {}
  }

  function saveSettings() {
    const settings = {
      darkTheme: themeSwitch.checked,
      length: parseInt(lengthSelect.value),
      numbers: numbersCheckbox.checked,
      uppercase: uppercaseCheckbox.checked,
      lowercase: lowercaseCheckbox.checked,
      hexadecimal: hexadecimalCheckbox.checked,
      excludeSimilar: excludeSimilarCheckbox.checked,
      enableSymbols: enableSymbolsCheckbox.checked,
      symbols: symbolsInput.value
    };
    localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
  }

  // Валидирует минимальный набор символов включён, отключает кнопку, если нет.
  function validateOptions() {
    // Шестнадцатеричные блокируют остальные группы кроме excludeSimilar
    if(hexadecimalCheckbox.checked) {
      numbersCheckbox.disabled = true;
      uppercaseCheckbox.disabled = true;
      lowercaseCheckbox.disabled = true;
    } else {
      numbersCheckbox.disabled = false;
      uppercaseCheckbox.disabled = false;
      lowercaseCheckbox.disabled = false;
    }
    // Проверяем выбранные группы (исключая excludeSimilar, symbols, custom символы)
    const groupsSelected = hexadecimalCheckbox.checked || numbersCheckbox.checked || uppercaseCheckbox.checked || lowercaseCheckbox.checked;
    generateBtn.disabled = !groupsSelected;
    generateBtn.setAttribute('aria-disabled', String(!groupsSelected));
    // Кнопка копирования доступна только если есть результат
    copyBtn.disabled = !resultEl.textContent || resultEl.textContent.length === 0;
    copyBtn.setAttribute('aria-disabled', copyBtn.disabled.toString());
  }

  symbolsInput.addEventListener('input', () => {
    // Фильтруем по печатным ASCII и максимум 30 символов
    const safeChars = /^[\u0021-\u007e]*$/;
    let filtered = [...symbolsInput.value].filter(ch => safeChars.test(ch));
    filtered = [...new Set(filtered)];
    if(filtered.length > 30) filtered = filtered.slice(0,30);
    const filteredStr = filtered.join('');
    if(filteredStr !== symbolsInput.value) symbolsInput.value = filteredStr;
    saveSettings();
  });

  [lengthSelect, numbersCheckbox, uppercaseCheckbox, lowercaseCheckbox,
    hexadecimalCheckbox, excludeSimilarCheckbox, enableSymbolsCheckbox, symbolsInput].forEach(elem => {
    elem.addEventListener('change', () => {
      saveSettings();
      validateOptions();
    });
  });

  themeSwitch.addEventListener('change', () => {
    applyTheme(themeSwitch.checked);
    saveSettings();
  });

  validateOptions();

  function getCharFromSet(set) {
    try {
      const idx = crypto.getRandomValues(new Uint32Array(1))[0] % set.length;
      return set.charAt(idx);
    } catch {
      // fallback (маловероятно)
      return set.charAt(Math.floor(Math.random() * set.length));
    }
  }

  function shuffleArray(arr) {
    try {
      for(let i = arr.length - 1; i > 0; i--) {
        const j = crypto.getRandomValues(new Uint32Array(1))[0] % (i + 1);
        [arr[i], arr[j]] = [arr[j], arr[i]];
      }
    } catch {
      // fallback - простой shuffle
      for(let i = arr.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [arr[i], arr[j]] = [arr[j], arr[i]];
      }
    }
  }

  function getScoreColor(score) {
    switch(score) {
      case 0: return 'red';
      case 1: return 'orangered';
      case 2: return 'orange';
      case 3: return 'green';
      case 4: return 'darkgreen';
      default: return 'black';
    }
  }

  function getScoreLabel(score) {
    switch(score) {
      case 0: return 'Очень слабый';
      case 1: return 'Слабый';
      case 2: return 'Средний';
      case 3: return 'Хороший';
      case 4: return 'Отличный';
      default: return 'Неопределён';
    }
  }

  function removeConsecutiveRepeats(str, maxRepeats = 2) {
    if (!str) return str;
    let result = str[0];
    let count = 1;
    for (let i = 1; i < str.length; i++) {
      if (str[i] === str[i - 1]) {
        count++;
        if (count <= maxRepeats) {
          result += str[i];
        }
      } else {
        count = 1;
        result += str[i];
      }
    }
    return result;
  }

  function showToast(message) {
    let toast = document.createElement('div');
    toast.textContent = message;
    toast.style.position = 'fixed';
    toast.style.bottom = '20px';
    toast.style.left = '50%';
    toast.style.transform = 'translateX(-50%)';
    toast.style.backgroundColor = 'rgba(50,50,50,0.85)';
    toast.style.color = 'white';
    toast.style.padding = '10px 20px';
    toast.style.borderRadius = '8px';
    toast.style.fontWeight = '600';
    toast.style.zIndex = '1000';
    toast.style.userSelect = 'none';
    toast.style.fontSize = '1rem';
    document.body.appendChild(toast);
    setTimeout(() => {
      toast.style.transition = 'opacity 0.5s';
      toast.style.opacity = '0';
    }, 1500);
    setTimeout(() => document.body.removeChild(toast), 2000);
  }

  function generatePassword() {
    const length = parseInt(lengthSelect.value);
    if(length < 12 || length > 64) {
      resultEl.textContent = 'Длина пароля должна быть от 12 до 64 символов';
      entropyEl.textContent = '';
      return;
    }
    const includeNumbers = numbersCheckbox.checked;
    const includeUppercase = uppercaseCheckbox.checked;
    const includeLowercase = lowercaseCheckbox.checked;
    const includeHex = hexadecimalCheckbox.checked;
    const excludeSimilar = excludeSimilarCheckbox.checked;
    const includeSymbols = enableSymbolsCheckbox.checked;
    let customSymbols = symbolsInput.value;

    customSymbols = [...new Set(customSymbols)].join('');

    const sets = {
      numbers: '0123456789',
      uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
      lowercase: 'abcdefghijklmnopqrstuvwxyz',
      hex: '0123456789ABCDEF',
      symbols: '!@#$%^&*()_+-=[]{}|;:\'",.<>?/\\',
    };

    const specialSet = '!@#$%^&*';

    let charset = '';

    if (includeHex) {
      charset = sets.hex;
    } else {
      if (includeNumbers) charset += sets.numbers;
      if (includeUppercase) charset += sets.uppercase;
      if (includeLowercase) charset += sets.lowercase;
    }

    if (customSymbols.length) charset += customSymbols;

    if (excludeSimilar) {
      charset = charset.split('').filter(ch => !'il1Lo0O'.includes(ch)).join('');
    }

    if (charset.length === 0) {
      resultEl.textContent = 'Выберите хотя бы одну группу символов!';
      entropyEl.textContent = '';
      return;
    }

    let passwordChars = [];
    let randomValues;
    try {
      randomValues = new Uint32Array(length);
      crypto.getRandomValues(randomValues);
    } catch {
      // fallback
      randomValues = new Uint32Array(length);
      for(let i=0; i<length; i++) randomValues[i] = Math.floor(Math.random() * 0xFFFFFFFF);
    }

    for (let i = 0; i < length; i++) {
      passwordChars.push(charset[randomValues[i] % charset.length]);
    }

    if (includeSymbols) {
      const maxSpecial = Math.min(2, length);
      let countSpecial = 1;
      try {
        countSpecial = 1 + (crypto.getRandomValues(new Uint8Array(1))[0] % 2);
      } catch {
        countSpecial = 1 + (Math.floor(Math.random()*2));
      }
      countSpecial = Math.min(countSpecial, maxSpecial);
      for (let i = 0; i < countSpecial; i++) {
        let pos;
        try {
          pos = crypto.getRandomValues(new Uint32Array(1))[0] % length;
        } catch {
          pos = Math.floor(Math.random() * length);
        }
        const symIdx = crypto.getRandomValues ? crypto.getRandomValues(new Uint32Array(1))[0] % specialSet.length : Math.floor(Math.random() * specialSet.length);
        passwordChars[pos] = specialSet[symIdx];
      }
    }

    let requiredChars = [];
    if (includeHex) {
      requiredChars.push(getCharFromSet(sets.hex));
    } else {
      if (includeNumbers) requiredChars.push(getCharFromSet(sets.numbers));
      if (includeUppercase) requiredChars.push(getCharFromSet(sets.uppercase));
      if (includeLowercase) requiredChars.push(getCharFromSet(sets.lowercase));
    }
    if (customSymbols.length) requiredChars.push(getCharFromSet(customSymbols));

    requiredChars.forEach(ch => {
      let pos, attempts = 0;
      do {
        try {
          pos = crypto.getRandomValues(new Uint32Array(1))[0] % length;
        } catch {
          pos = Math.floor(Math.random() * length);
        }
        attempts++;
      } while (includeSymbols && specialSet.includes(passwordChars[pos]) && attempts < 10);
      passwordChars[pos] = ch;
    });

    shuffleArray(passwordChars);
    let password = passwordChars.join('');
    password = removeConsecutiveRepeats(password, 2);

    while(password.length < length) {
      const idx = crypto.getRandomValues ? crypto.getRandomValues(new Uint32Array(1))[0] % charset.length : Math.floor(Math.random()*charset.length);
      password += charset[idx];
    }

    password = password.split('').sort(() => 0.5 - Math.random()).join('');
    resultEl.textContent = password;

    let evalResult = null;
    try {
      evalResult = zxcvbn(password);
    } catch {}

    const entropy = (evalResult && typeof evalResult.entropy === 'number') ? Math.round(evalResult.entropy) : 0;
    const score = (evalResult && typeof evalResult.score === 'number') ? evalResult.score : 0;

    let entropyDisplay;
    if (score >= 3) {
      entropyDisplay = 'Высокая надёжность';
    } else if (entropy <= 0 && score > 0) {
      entropyDisplay = '10+ бит';
    } else {
      entropyDisplay = entropy + ' бит';
    }
    entropyEl.innerHTML = `Оценка надежности: <b>${entropyDisplay}</b><br>
      Уровень: <b style="color:${getScoreColor(score)}">${getScoreLabel(score)}</b>`;

    validateOptions();
  }

  function escapeHTML(str) {
    return str.replace(/[&<>"']/g, function(m) {
      return {'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', '\'':'&#39;'}[m];
    });
  }

  function checkPasswordStrength() {
    const pwd = checkPasswordInput.value.trim();
    if(!pwd) {
      checkResult.textContent = "Введите пароль для проверки.";
      checkResult.style.color = "black";
      checkSuggestions.textContent = '';
      return;
    }
    if(pwd.length > 128) {
      checkResult.textContent = "Пароль слишком длинный (максимум 128 символов).";
      checkResult.style.color = "red";
      checkSuggestions.textContent = '';
      return;
    }
    let evalResult = null;
    try {
      evalResult = zxcvbn(pwd);
    } catch {}

    const entropyCheck = (evalResult && typeof evalResult.entropy === 'number') ? Math.round(evalResult.entropy) : 0;
    const scoreCheck = (evalResult && typeof evalResult.score === 'number') ? evalResult.score : 0;

    let entropyDisplayCheck;
    if (scoreCheck >= 3) {
      entropyDisplayCheck = 'Высокая надёжность';
    } else if (entropyCheck <= 0 && scoreCheck > 0) {
      entropyDisplayCheck = '10+ бит';
    } else {
      entropyDisplayCheck = entropyCheck + ' бит';
    }
    checkResult.innerHTML = `Оценка надежности: <b>${entropyDisplayCheck}</b><br>
      Уровень: <b style="color:${getScoreColor(scoreCheck)}">${getScoreLabel(scoreCheck)}</b>`;

    if (evalResult && (evalResult.feedback.warning || (evalResult.feedback.suggestions && evalResult.feedback.suggestions.length > 0))) {
      checkSuggestions.innerHTML = '';
      if(evalResult.feedback.warning) {
        checkSuggestions.innerHTML += `<div>\u26a0\ufe0f <b>Предупреждение:</b> ${escapeHTML(translateWarning(evalResult.feedback.warning))}</div>`;
      }
      if (evalResult.feedback.suggestions && evalResult.feedback.suggestions.length > 0) {
        checkSuggestions.innerHTML += `<div>\u1f4a1 <b>Рекомендации:</b><ul>${evalResult.feedback.suggestions.map(s=>`<li>${escapeHTML(translateSuggestion(s))}</li>`).join('')}</ul></div>`;
      }
    } else {
      checkSuggestions.textContent = '';
    }
  }

  generateBtn.addEventListener('click', () => {
    generateBtn.disabled = true;
    generateBtn.setAttribute('aria-disabled', 'true');
    setTimeout(() => {
      generatePassword();
      generateBtn.disabled = false;
      generateBtn.setAttribute('aria-disabled', 'false');
    }, 50);
  });

  copyBtn.addEventListener('click', () => {
    const pwd = resultEl.textContent;
    if(!pwd || pwd === 'Выберите хотя бы одну группу символов!' || pwd === 'Длина пароля должна быть от 12 до 64 символов') return;
    navigator.clipboard.writeText(pwd).then(() => showToast('Пароль скопирован в буфер обмена!'));
  });

  checkBtn.addEventListener('click', () => {
    checkResult.classList.add('hidden');
    setTimeout(() => {
      checkPasswordStrength();
      checkResult.classList.remove('hidden');
    }, 200);
  });

  checkPasswordInput.addEventListener('input', () => {
    if (checkPasswordInput.value.trim() === '') {
      checkResult.textContent = '';
      checkSuggestions.textContent = '';
      return;
    }
    if(checkPasswordInput.value.length > 128){
      checkResult.textContent = "Пароль слишком длинный (максимум 128 символов).";
      checkResult.style.color = "red";
      checkSuggestions.textContent = '';
      return;
    }
    checkPasswordStrength();
  });

  loadSettings();
  validateOptions();
});
</script>
</body>
</html>